package uk.ac.aber.paws.client.state;

import java.util.Calendar;
import java.util.Hashtable;
import java.util.Vector;

import uk.ac.aber.paws.client.constants.Constants;
import uk.ac.aber.paws.client.core.PAWSClient;
import uk.ac.aber.paws.client.db.DBLogger;

/**
 * @author Rob Johnson, Aberystwyth University
 * @email rob.johnson@aber.ac.uk
 * @date 16th August 2011
 * 
 * LoginRegister.java
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

public class LoginRegister {

	private Vector<Login> logins;
	
	/**
	 * If a Login object does not exist for a user when an idle time is registered, this flag decides
	 * whether this can register a login object automatically. If a windows system, then this flag should be false 
	 * as qwinsta is required to identify and register the login object. For Linix and MAC, it depends
	 * entirely on the information collated by the IdleLogger program, therefore this flag should be set to true for these O/S's 
	 */
	private boolean autoRegister = false;
	
	/**
	 * This flag identifies if a database failure has occurred whilst trying to log login information to the
	 * database. This flag is queried by the sleep receptor. If a database failure has occurred, then it
	 * will ensure that the computer is not slept.
	 */
	private boolean problemsDetected = false;
	
	public LoginRegister() {
		
		logins = new Vector<Login>();
		
		initialise();
	}
	
	/**
	 * Initialises the autoRegister parameter. This is important for determining the login logging
	 * mechanism for the client. If autoRegister is set to false, then the data collected from IdleLogger
	 * is not used to register user login sessions. This is typical in Windows installations by which the 
	 * login info is collected from qwinsta and IdleLogger only updates the idle time for the user within
	 * the Login structure. Should the computer be Linux or MAC, then we are fully dependent on the data
	 * collected from IdleLogger and therefore the idle time reporting creates the user session if it does
	 * not already exist.
	 */
	private void initialise() {
		
		String os = PAWSClient.SYSTEM_STATE.getComputerInfo().getOperatingSystem();
		
		// Windows operating systems
		if (os.equals(Constants.OS_FLAG_WINDOWS7) ||
				os.equals(Constants.OS_FLAG_WINDOWSXP) ||
				os.equals(Constants.OS_FLAG_WINDOWSVISTA)) {
			
			this.autoRegister = false;
		
		}
		// Linux and Mac operating systems
		else {
			// clear out expired sessions - that haven't idle logged for set period
			this.autoRegister = false;
		}
	}
	
	/**
	 * This is only called by on a MAC or Linux operating system. It automatically logs out sessions
	 * which have not been reported back from IdleLogger for 2 consecutive intervals. Say IdleLogger is
	 * configured to report idle times every 5 seconds backs to PAWS Client, then if the lastUpdated
	 * variable is greater than the current time less 10 seconds, then the session is assumed to have
	 * ended and the user session is logged off. 
	 */
	public void checkForExpiredSessions() {
	
		long expiryThreshold = 1000*(Integer.parseInt(PAWSClient.getConfig().getProperty("app.main.processinterrupts.idleloggerreport"))*2);
		long now = Calendar.getInstance().getTimeInMillis();
		
		// parse each login objeck
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			// if the user hasn't registered within twice the report back period, assume logged out
			if (obj.getLastUpdated() < now-expiryThreshold) {
				obj.closeSession();
			}
		}
		
	}
	
	/**
	 * This procedure logs out all users sessions informing the database to close each user session. This
	 * is typically called when the PAWS Client is stopped or computer is shut down..
	 */
	public void logOutAllUsers() {
		
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			// if the user hasn't registered within twice the report back period, assume logged out
			if (!obj.getDBClosed()) {
				obj.closeSession();
			}
		}
	}
	
	/**
	 * This is required to pass back the current idle times of logged in users back to PAWS Server.
	 * @return The current idle times of all users on the system
	 */
	public Hashtable<String,Long> getIdleTimes() {
	
		Hashtable<String,Long> returnInfo = new Hashtable<String,Long>();
		
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			returnInfo.put(obj.getUserID(),obj.getIdleTime());
		}
		
		return returnInfo;
	
	}
	
	/**
	 * This is repeatedly called on just Windows operating systems. Once a qwinsta command has been run on the
	 * windows system, the login list is passed into this function and parsed accordingly. If the user
	 * does not exist in the Logins structure, then a new object is created. If already exists, then the 
	 * lastUpdated variable is updated. At the end of the procedure, then closeWindowsStaleSeesions is run that
	 * clears up any Login sessions that are no longer active.
	 * 
	 * @param loginList A vector containing the user logins list
	 */
	public void processLogins(Vector<String> loginList) {
		
		long lastProcessed = Calendar.getInstance().getTimeInMillis();
		
		// for each login given by qwinsta
		for (int i=0;i<loginList.size();i++) {
			
			Login loginObj = loginExist(loginList.get(i));
			
			// the login session already exists for the given user, update the lastUpdated time
			if (loginObj != null) {
				loginObj.markLastUpdated(lastProcessed);
			}
			// create a new login session object
			else {
				// add the new user sessions
				logins.add(new Login(loginList.get(i).toUpperCase(),lastProcessed));
				
			}
		}
		
		// parse all active login sessoions and logout any that haven't been updated within this run
		closeWindowsStaleSessions(lastProcessed);
	}
	
	/**
	 * This procedure is important for closing any login sessions that are no longer in operation
	 * (according to qwinsta). If in the last run, the user session lastUpdated value was not updated, then
	 * the session is cleared - this assuming that the user has logged out because qqinsta did not detect the login.
	 * It also clears user sessions that were logged off over an hour ago.
	 * @param lastProcessed The benchmark time used for this run. The user session lastProcessed must match this to survive.
	 */
	private void closeWindowsStaleSessions(long lastProcessed) {
		
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			// if the user session was logged off over an hour ago
			if (obj.isOldRecord()) {
				logins.remove(i);
				closeWindowsStaleSessions(lastProcessed);
			}
			
			// if the user session is stale and currently not logged off, log it off
			if (obj.getLastUpdated() != lastProcessed && !obj.DBClosed) {
				obj.closeSession();
			}
		}
	}
	
	/**
	 * This is called subsequent to an IdleLogger report back. The current idle time of the user is
	 * stored in the Logins object. For a windows system, only the idle time is logged for the user if
	 * it exists. For Linux and MAC, if the user session does not exist, then this procedure forces a new
	 * Login object session creation and stores the idle time.
	 * @param userID The user ID (in upper case) to be logged
	 * @param idleTime The current idle time of the user
	 */
	public void registerIdleTime(String userID, long idleTime) {
	
		// benchmark time
		long lastProcessed = Calendar.getInstance().getTimeInMillis();
		
		// check if the user login session exists already
		Login loginObj = loginExist(userID.toUpperCase());
		
		// the user login session exists
		if (loginObj != null) {
			
			// update the idle time for this user
			loginObj.setIdleTime(idleTime);
			
			// only if Linix or MAC, reassign the lastUpdated variable. We don't do Windows one's here
			// or else it will interfere with the closeWindowsStaleSessions operation
			if (autoRegister) {
				loginObj.markLastUpdated(lastProcessed);
			}
		}
		
		// if Linux or MAC system and login obj does not exist...add login session
		else {
			if (autoRegister) {
				Login newLoginObj = new Login(userID.toUpperCase(),lastProcessed);
				logins.add(newLoginObj);
				newLoginObj.setIdleTime(idleTime);
				newLoginObj.markLastUpdated(lastProcessed);
			}
		}
 		
	}
	
	/**
	 * Returns whether the Loginregister has experienced logging to the database. If so, this flag
	 * can be used to stop inadvertent sleeping of workstations.
	 * @return True if there are problems connecting to the database.
	 */
	public boolean getProblemsDetected() {
		return problemsDetected;
	}
	
	/**
	 * This searches the current Login structure for a giver user id
	 * @param login The userID to be searched
	 * @return Null if the user object is not found, otherwise reference to the login object containing all the data
	 */
	private Login loginExist(String login) {
		
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			// update the login session that is already in play
			if (obj.getUserID().equals(login) && !obj.getDBClosed()) {	
				return obj;
			}
		}
		
		return null;
	}
	
	/**
	 * This is called by an external program for passing back the current contents of the Logins object.
	 * @return The internal data detailing all sessions.
	 */
	public Vector<Hashtable<String,Object>> getLoginInfo() {
		
		Vector<Hashtable<String,Object>> returnInfo = new Vector<Hashtable<String,Object>>();
		
		for (int i=0;i<logins.size();i++) {
			
			Login obj = logins.get(i);
			
			returnInfo.add(obj.getInfo());
		}
		
		return returnInfo;
	}
	
	class Login {
		
		private String userID;
		private long dBID = -1;
		private long loginTime = -1;
		private long idleTime = -1;
		private long lastUpdated = -1;
		private long logoutTime = -1;
		private boolean DBClosed = false;
		
		public Login(String userID, long lastProcessed) {
			newLogin(userID);
			this.lastUpdated = lastProcessed;  
		}
		
		private void newLogin(String userID) {
			
			this.userID = userID;
			this.loginTime = Calendar.getInstance().getTimeInMillis();
			this.dBID = new DBLogger().registerNewUserLogin(this.userID,this.loginTime);
			
			if (this.dBID == -1) {
				problemsDetected = true;
			}
			else {
				problemsDetected = false;
			}
		}
		
		public Hashtable<String,Object> getInfo() {
			
			Hashtable<String,Object> returnObj = new Hashtable<String,Object>();
			
			returnObj.put("UserID", this.userID);
			returnObj.put("DBID", this.dBID);
			returnObj.put("LoginTime", this.loginTime);
			returnObj.put("IdleTime", this.idleTime);
			returnObj.put("LastUpdated", this.lastUpdated);
			returnObj.put("LogoutTime", this.logoutTime);
			returnObj.put("DBClosed", this.DBClosed);
			
			return returnObj;
		}
		
		public long checkDB() {
			
			return this.dBID;
		}
		
		public String getUserID() {
			return userID;
		}
		
		public void setIdleTime(long idleTime) {
			this.idleTime = idleTime;
		}
		
		public long getIdleTime() {
			return this.idleTime;
		}
		
		public boolean isOldRecord() {
			
			// delete any records old than hour
			if (this.DBClosed && this.logoutTime < (Calendar.getInstance().getTimeInMillis()-3600000)) {
				return true;
			}
			
			return false;
		}
		
		public void closeSession() {
			
			this.DBClosed = true;
			this.logoutTime = Calendar.getInstance().getTimeInMillis();
			new DBLogger().registerUserLogout(this.dBID);
			
		}
		
		public void markLastUpdated(long lastProcessed) {
			lastUpdated = lastProcessed;
		}
		
		public long getLastUpdated() {
			return lastUpdated;
		}
		
		public boolean getDBClosed() {
			return DBClosed;
		}
	}
	
}
